package jxtend.processor.eclipse.visitor.replace;

import jxtend.processor.eclipse.utils.ASTNodeUtils;
import jxtend.processor.eclipse.utils.EclipseProcessorUtils;
import jxtend.processor.eclipse.visitor.handler.impl.ASTNodeBits;
import jxtend.processor.reflect.ReflectField;

import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.LocalDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;


@SuppressWarnings("restriction")
public class AnnotationReplaceHandler implements ASTNodeReplaceHandler {

	@Override
	public boolean replaceNode(ASTNode parentNode, ASTNode originalNode, ASTNode newNode) {
		if (!(originalNode instanceof Annotation) || !(newNode instanceof Annotation)) {
			return false;
		}

		if (parentNode instanceof AbstractMethodDeclaration) {
			replaceAnnotation(parentNode, (Annotation) originalNode, (Annotation) newNode);
		} else if (parentNode instanceof Argument) {
			replaceAnnotation(parentNode, (Annotation) originalNode, (Annotation) newNode);
		} else if (parentNode instanceof FieldDeclaration) {
			replaceAnnotation(parentNode, (Annotation) originalNode, (Annotation) newNode);
		} else if (parentNode instanceof LocalDeclaration) {
			replaceAnnotation(parentNode, (Annotation) originalNode, (Annotation) newNode);
		} else if (parentNode instanceof TypeDeclaration) {
			replaceAnnotation(parentNode, (Annotation) originalNode, (Annotation) newNode);
		} else {
			return false;
		}

		return true;
	}
	
	private static void replaceAnnotation(ASTNode parentNode, Annotation orginalAnnotation, Annotation newAnnotation) {
		Class<ASTNode> classOfParentNode = ASTNodeUtils.getClassForNormalKindASTNode(parentNode);
		ReflectField<ASTNode, Annotation[]> annotationsField =
			ReflectField.getPublicField(classOfParentNode, Annotation[].class, "annotations");

		Annotation[] annotations = annotationsField.get(parentNode);
		if (annotations == null) {
			throw new IllegalStateException("Could not replace annotation, since the annotation array was null");
		}

		int indexOfOriginalAnnotation = -1;
		for (int i = 0; i < annotations.length; i++) {
			if (annotations[i] != orginalAnnotation) { continue; }

			if (indexOfOriginalAnnotation != -1) {
				throw new IllegalStateException(
						"More then one anotation exists with the same identity as the to be replaced annotation");
			}

			indexOfOriginalAnnotation = i;
		}

		if (indexOfOriginalAnnotation == -1) {
			throw new IllegalStateException(
					"Could not replace annotation, it does not exist in the annotation array");
		}

		boolean keepOriginalAnnotation = false;
		if (Thread.currentThread().getName().equals("org.eclipse.jdt.internal.ui.text.JavaReconciler")) {
			ReferenceBinding originalTypeReference = (orginalAnnotation.type.resolvedType instanceof ReferenceBinding)
					? (ReferenceBinding) orginalAnnotation.type.resolvedType : null;
			ReferenceBinding newTypeReference = (newAnnotation.type.resolvedType instanceof ReferenceBinding)
					? (ReferenceBinding) newAnnotation.type.resolvedType : null;

			keepOriginalAnnotation = (originalTypeReference != null)
				&& EclipseProcessorUtils.isValidTypeReference(originalTypeReference) && (newTypeReference != null)
				&& EclipseProcessorUtils.isValidTypeReference(newTypeReference)
				&& (originalTypeReference != newTypeReference); 
		}

		if (keepOriginalAnnotation) {
			Annotation[] newAnnotations = new Annotation[annotations.length + 1];

			System.arraycopy(annotations, 0, newAnnotations, 0, indexOfOriginalAnnotation + 1);

			newAnnotation.bits |= ASTNodeBits.JXTEND_GENERATED;
			newAnnotations[indexOfOriginalAnnotation + 1] = newAnnotation;

			System.arraycopy(annotations, indexOfOriginalAnnotation + 1, newAnnotations,
					indexOfOriginalAnnotation + 2, annotations.length - (indexOfOriginalAnnotation + 1));

			annotationsField.set(parentNode, newAnnotations);
		} else {
			annotations[indexOfOriginalAnnotation] = newAnnotation;
		}
	}
}
